/*
 * Copyright 2012 Marsrutai.lt
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package lt.marsrutai.mobile.android.service.impl;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import lt.marsrutai.mobile.android.service.LocationProvider;
import lt.marsrutai.mobile.android.util.StringUtils;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;

/**
 * Manages listening for {@link Location} updates on multiple providers and
 * maintains {@link #currentBestLocation current best location fix}.
 * 
 * @author Justas
 */
public class LocationProviderImpl implements LocationProvider {

    private static final int TWO_MINUTES = 1000 * 60 * 2;
    
    private LocationManager locationManager;

    /**
     * Stores location update requests and their status:
     * <ul>
     * <li><code>true</code> - expected to provide location updates (listener registered)</li>
     * <li><code>false</code> - not expected to provide location updates (listener removed, or provider disabled)</li>
     * </ul>
     */
    private Map<LocationUpdateRequest, Boolean> requestStatusMap = new HashMap<LocationUpdateRequest, Boolean>();
    
    /**
     * Best location fix at this particular moment. Is updated only when a better (more precise
     * or significantly newer) location fix is received
     */
    private Location currentBestLocation;
    
    /**
     * Default location listener which updates {@link #currentBestLocation} if a new location is better
     */
    private LocationListener locationListener = new LocationListener() {

        public void onLocationChanged(Location location) {
            if (isBetterLocation(location, currentBestLocation)) {
                currentBestLocation = location;
            }
        }

        public void onStatusChanged(String provider, int status, Bundle extras) {
        }

        public void onProviderEnabled(String provider) {
            setStatus(provider, Boolean.TRUE);
        }

        public void onProviderDisabled(String provider) {
            setStatus(provider, Boolean.FALSE);
        }
    };

    /**
     * Constructor just stores {@link LocationUpdateRequest}s. To start
     * listening for location updates {@link #start()} must be called.
     * 
     * @param context
     * @param requests
     *            Array of {@link LocationUpdateRequest}s each representing a
     *            listener configuration for location manager
     */
    public LocationProviderImpl(Context context, LocationUpdateRequest[] requests) {
        for (LocationUpdateRequest locationUpdateRequest : requests) {
            setStatus(locationUpdateRequest, Boolean.FALSE);
        }
        
        locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
    }
    
    public void start() {
        // Register the listener with the Location Manager to receive location updates
        for (Entry<LocationUpdateRequest, Boolean> entry : requestStatusMap.entrySet()) {
            LocationUpdateRequest request = entry.getKey();
            Boolean enabled = entry.getValue();
            
            if (!enabled) {
                // register listeners only for enabled providers
                boolean providerEnabled = false;
                try {
                    providerEnabled = locationManager.isProviderEnabled(request.provider);
                }
                catch (Exception ex) {
                }
    
                if (providerEnabled) {
                    locationManager.requestLocationUpdates(request.provider, request.minTime, request.minDistance, locationListener);
                    setStatus(request, Boolean.TRUE);
                }
            }
        }
        
    }

    public void stop() {
        locationManager.removeUpdates(locationListener);

        // All listeners are removed, requestStatusMap should reflect that, setting status to FALSE
        for (LocationUpdateRequest request : requestStatusMap.keySet()) {
            setStatus(request, Boolean.FALSE);
        }
    }

    @Override
    public Location getCurrentBestLocation() {
        return currentBestLocation;
    }
    
    public boolean isReceivingUpdates() {
        for (Boolean requestEnabled : requestStatusMap.values()) {
            if (requestEnabled) {
                return true;
            }
        }
        return false;
    }

    /**
     * Set the provided <code>status</code> for all
     * {@link LocationUpdateRequest}s for <code>provider</code>
     * 
     * @param provider
     *            Location provider name
     * @param status
     *            Status indicating whether requests for the provided location
     *            provider is expected to provide location updates
     */
    protected void setStatus(String provider, Boolean status) {
        for (Entry<LocationUpdateRequest, Boolean> entry : requestStatusMap.entrySet()) {
            if (StringUtils.equals(entry.getKey().provider, provider)) {
                requestStatusMap.put(entry.getKey(), status);
            }
        }
    }

    /**
     * Set the provided <code>status</code> for a particular
     * {@link LocationUpdateRequest}
     * 
     * @param locationUpdateRequest
     * @param status
     *            Status indicating whether requests for the provided
     *            <code>locationUpdateRequest</code> is expected to provide
     *            location updates
     */
    protected void setStatus(LocationUpdateRequest locationUpdateRequest, Boolean status) {
        requestStatusMap.put(locationUpdateRequest, status);
    }

    /**
     * Determines whether one Location reading is better than the current
     * Location fix
     * 
     * @param location
     *            The new Location that you want to evaluate
     * @param currentBestLocation
     *            The current Location fix, to which you want to compare the new
     *            one
     * @see http://developer.android.com/guide/topics/location/strategies.html
     */
    protected boolean isBetterLocation(Location location, Location currentBestLocation) {
        if (currentBestLocation == null) {
            // A new location is always better than no location
            return true;
        }

        // Check whether the new location fix is newer or older
        long timeDelta = location.getTime() - currentBestLocation.getTime();
        boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
        boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
        boolean isNewer = timeDelta > 0;

        /*
         * If it's been more than two minutes since the current location, use
         * the new location because the user has likely moved
         */
        if (isSignificantlyNewer) {
            return true;
            // If the new location is more than two minutes older, it must be worse
        }
        else if (isSignificantlyOlder) {
            return false;
        }

        // Check whether the new location fix is more or less accurate
        int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
        boolean isLessAccurate = accuracyDelta > 0;
        boolean isMoreAccurate = accuracyDelta < 0;
        boolean isSignificantlyLessAccurate = accuracyDelta > 200;

        // Check if the old and new location are from the same provider
        boolean isFromSameProvider = isSameProvider(location.getProvider(), currentBestLocation.getProvider());

        // Determine location quality using a combination of timeliness and accuracy
        if (isMoreAccurate) {
            return true;
        }
        else if (isNewer && !isLessAccurate) {
            return true;
        }
        else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
            return true;
        }
        return false;
    }

    /**
     * Checks whether two providers are the same
     * 
     * @param provider1
     * @param provider2
     * @return
     */
    private boolean isSameProvider(String provider1, String provider2) {
        if (provider1 == null) {
            return provider2 == null;
        }
        return provider1.equals(provider2);
    }
    
    /**
     * Used for registering listeners for location updates. Properties of this class correspond to
     * {@link LocationManager#requestLocationUpdates(String, long, float, LocationListener)}
     * 
     * @author Justas
     */
    public static class LocationUpdateRequest {
        
        String provider;
        long minTime;
        float minDistance;
        
        public LocationUpdateRequest(String provider, long minTime, float minDistance) {
            this.provider = provider;
            this.minDistance = minDistance;
            this.minTime = minTime;
        }
    }
}
